####
#
# Copyright 2007 B. Chandler Carruth
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
####
#
# ./lib/rcons/target.rb
#
# Base target class for forming build graphs.
#
####


# The RCons module packages together all of the components of the
# RCons build system.  The system utilizes the Ruby language context
# to ease the construction of and execution of build files, which specify
# the procedure necessary to compile source files into an executable
# application.

module RCons
  # The base class for creating build graphs.  This class should never be
  # initialized, but instead extended to more concrete classes.
  #
  # Along with any other extension-specific functions, the following
  # functions should be implemented in any extension classes:
  #
  #
  # - +build_target+
  class Target
    attr_reader( :name, :brief, :long )

    # Initializes a target object.  At this level, the function is very
    # generic and should be used in extension classes along with
    # any other modifications.
    #
    # Inputs:
    #
    # +build_env+:: an +Environment+.
    # +name+:: the name of the target.
    # +opts+:: a list of options.
    def initialize( build_env, name, opts = Hash.new() )
      # Set up default target variables
      @build_env = build_env
      @name = name.to_s()

      @brief = "#{@name} build target"
      @brief = opts[ :brief ].to_s() if opts[ :brief ]

      @long = "This is the #{@name} build target object."
      @long = opts[ :long ].to_s() if opts[ :long ]


      # Initialize dependency arrays so we can push and pop
      @pre_deps = Array.new()
      @post_deps = Array.new()

      # Setup default deps
      add_pre_dep( @build_env )

      # set up scratch root
      @scratch_root = File.join( '$(SCRATCH_ROOT)', @name )

      # set up the builder object
      if opts[ :builder ]
        @builder = opts[ :builder ]
      else
        if opts[ :builder_class ]
          builder_class = opts[ :builder_class ]
        else
          builder_class = self.class::Builder
        end
        @builder = builder_class.get_new( @build_env, @name, @scratch_root )
      end
    end

    # Adds a target to the list of pre-dependencies for the current target.
    #
    # Inputs:
    # +target+:: the target to be added.  Must be of class +Target+.
    def add_pre_dep( target )
      unless target.kind_of?( Target )
        raise( ArgumentError, "Unexpected argument type for target", caller() )
      else
        @pre_deps.push( target ) unless @pre_deps.include?( target )
      end
    end

    # Adds a target to the list of post-dependencies for the current target.
    #
    # Inputs:
    # +target+:: the target to be added.  Must be of class +Target+.
    def add_post_dep( target )
      unless target.kind_of?( Target )
        raise( ArgumentError, "Unexpected argument type for target", caller() )
      else
        @post_deps.push( target ) unless @post_deps.include?( target )
      end
    end

    # Builds any pre-dependencies associated with the target.  As is, this
    # function is generic and should be implemented in extension classes.
    def build_pre_deps( changed )
      for d in @pre_deps
        return false unless d.build() { |c| if c then changed = true end }
      end

      yield( changed ) if block_given?
      true
    end

    def build_target( changed )
      yield( changed ) if block_given?
      true
    end

    # Builds any post-dependencies associated with the target.  As is, this
    # function is generic and should be implemented in extension classes.
    def build_post_deps()
      for d in @post_deps
        return false unless d.build()
      end
      true
    end

    # Invokes the build process, making sure pre-dependencies are built,
    # building the target, and finally any post-dependencies.
    def build()
      changed = false

      unless @builder.built?
        # build all pre-dependencies
        return( false ) unless build_pre_deps( changed ) { |c| changed = c }

        # We build the target, giving it the change information
        return( false ) unless @builder.do_build( changed )

        # build post-deps, providing them the current changed status
        return( false ) unless build_post_deps()

        @build_env.print( "Built '#{@name}'" + if @builder.changed? then ' (changed)' else '' end  )
      end

      yield( @builder.changed? ) if block_given?

      return( true )
    end

    # The class for building targets. We pull any build-related
    # functionality of targets into objects which can remain uniquely
    # identified, and be referenced by targets by name.
    class Builder
      attr_reader( :name )

      # This is a class method to get a new builder for a target. It
      # attempts to re-use any existing environment-name pair, and
      # otherwise creates a new one, and registers it for use later.
      def Builder.get_new( build_env, name, *args )
        b = build_env.lookup_builder( name )

        unless b
          b = self.new( build_env, name, *args )
        end

        unless b.instance_of?( self )
          raise( ArgumentError,
                 "ERROR: Mismatched Builder Type and Name! (#{b.class} != #{self})",
                 caller() )
        end

        b
      end

      # The initialize method simply allows the target to move some
      # minimal information into the builder. This method should never,
      # repeat *never* be overridden by a subclass Builder.
      def initialize( build_env, name, scratch_path )
        @build_env = build_env
        @name = name
        @scratch_path = scratch_path

        @built = false
        @changed = false

        @build_env.register( self )
      end

      # Begins the build process for the target.  This method specifies
      # exactly how a target should be built, and will be called by the
      # +build()+ method.  This should be implemented
      # in classes that extend +Target+.
      def do_build( has_changed )
        @changed = has_changed
        @built = true
      end

      # Predicate method to check status of build
      def built?
        @built
      end

      # Predicate method to check changes due to build
      def changed?
        @changed
      end
    end

    # Provides a string representation of the target object.
    def to_s()
      @name
    end
  end
end

